package transfer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.BitSet;

import core.Util;

import network.NetworkVars;

/**
 *	This class contains the protocol for transferring files. It works like so:
 * <br>
 * 	Ensure both ends are using an accurate hashing algorithm (chosen ahead of time), and they generate the same results.
 * 		(Make sure they generate the same results, use a random test string)
 * <br>
 * 	Each fragment is sent, along with it's hash value. each is checked to ensure correctness
 *[Fragment protocol]
 *<br>
 *		Fragments are set up as such:
 *<br>
 *	[x]/[total size]:[bytes of set length]@[hash]
 *	there are a few 'special' fragments
 *<br>
 *	n/...
 *		this fragment give the file name in the bytes, and does not adhere to the set length, has a hash
 *<br>
 *	h/...
 *		this fragment gives the Total file hash, also does not adhere to the set length, has no hash
 *<br>
 *	m/...
 *		this fragment contains miscellaneous data, does not adhere to set length, has no hash.
 *<br>
 *		info contained is:
 *			<br>d#		#=number of data bytes per fragment(incase it isn't the default)
 *			<br>l#		#=number of bytes in the file
 *			<br>f#		#=total number of fragments
 *
 *		
 * @author Gianni Gambetti
 *
 */

/*	
 * Problem: using integers caps the size of the file usable. Just under 2Gb(by 1-2 bytes), not too shabby for size. but could be a problem later.
 * Note: due to the above, loading the file into ram will not be done. it's a very bad idea. ei: if you do, you wouldn't have any more ram.
 * 
 */

public class FileTransfer {

	public static final long MAX_FRG_SIZE = Integer.MAX_VALUE;
	public static final int MAX_FRGS = Integer.MAX_VALUE;

	private BitSet have = null;
	private boolean done = false;//set this to true when we have captured the whole file, only when not sending
	
	private boolean serv = false;
	
	private int frag_Size = NetworkVars.fileChunk;
	
	private int numFrag = -3;//current fragment
	private int totFrags = 0;

	//info for transfer
	private String hash = "";//hash for the whole thing
	private FileFragment[] info = null;

	//IO
	private File file;
	private RandomAccessFile inOut = null;

	public static void main(String[] args) {
		System.out.println(NetworkVars.encode+": "+getHash("This is a test"));

		try {
			FileTransfer send = new FileTransfer("res/beep_1.au",true);
			FileTransfer recv = new FileTransfer("",false);

			int j=-3;
			while(j < send.totFrags*2){
				System.out.println("-----");
				FileFragment frg = send.nextFrag();
				System.out.println(frg.toString());
				System.out.println("LEN:"+frg.getData().length);
				recv.saveFrag(frg);
				j++;
			}


		} catch (IOException e) {
			e.printStackTrace();
		}

	}




	/**
	 * 
	 * @param f this denotes which file to transfer
	 * @param ram if true, this will load the file+associated parts into ram, causing a ram intensive process,
	 *  but single read etc. if false it will read once for each broadcast, and compute MD5s every time.
	 */
	public FileTransfer(String f, boolean send) throws IOException{
		info = new FileFragment[3];//both send/receive will need this.
		if(send){
			serv = true;
			file = new File(f);
			inOut = new RandomAccessFile(file,"r");

			long sz = file.length();//size in bytes of the file
			//the number of fragments is equal to the size/chunk size.
			if( sz/frag_Size > MAX_FRGS){
				//whoa, file is bigger then max fragments!
				//increase fragment size.
				do{
					frag_Size = frag_Size << 1;//double fragment Size.
					if(frag_Size < 0 || frag_Size > MAX_FRG_SIZE){
						//frag size got so big it's negative, or it's too big.
						//error, can't send this file.
						throw new IOException("File is too large to transfer:"+file.getAbsolutePath()+" @"+file.length());
					}
				}while(sz/frag_Size > MAX_FRGS);
			}
			totFrags = roundUp( (sz*1.0)/frag_Size );
			if(!file.canRead()){
				//error
				throw new IOException("Can't read the file:"+file.getAbsolutePath());
			}

			//build a few framents
			hash = getHash(file);
			
			//TODO Info Fragments
			info[0] = FileFragment.buildFragment(file.getName(), FileFragment.NAME_FRG, 0, totFrags);
			info[1] = FileFragment.buildFragment("d"+frag_Size+"l"+file.length()+"f"+totFrags, FileFragment.MISC_FRG, 0, totFrags);
			info[2] = FileFragment.buildFragment(hash, FileFragment.HASH_FRG, 0, totFrags);
			
			

		}else{
			file = new File("TMP");
			inOut = new RandomAccessFile(file,"rwd");
		}
	}

	/**
	 * This is for broadcasting the fragments, and can't be used if you are receiving them.
	 */
	public FileFragment nextFrag(){
		if( !serv ){
			return null;
		}
		FileFragment f=null;
		if(NetworkVars.debug){
			System.out.println(numFrag);
		}

		f=getFrag(numFrag);

		numFrag++;
		if(numFrag >= totFrags){
			numFrag = -3;
		}

		return f;
	}

	/*
	 	info[0] = FileFragment.buildFragment(file.getName(), FileFragment.NAME_FRG, 0, totFrags);
		info[1] = FileFragment.buildFragment("d"+frag_Size+"l"+file.length()+"f"+totFrags, FileFragment.MISC_FRG, 0, totFrags);
		info[2] = FileFragment.buildFragment(hash, FileFragment.HASH_FRG, 0, totFrags);
			
	 */
	/**
	 * Returns the desired fragment, where -3 < i < totFrags
	 * @param i the desired fragment, -3 for the Hash fragment, -2 for the misc fragment, -1 for the name fragment, all other values return the fragment of the file at position i*frag_Size
	 * @return a file fragment containing the data from the file.
	 */
	public FileFragment getFrag(int i){
		if( !serv ){
			return null;
		}
		FileFragment f = null;
		if(numFrag < 0){
			f = info[(int)-numFrag-1];//0-2
		}else{

			byte[] data = new byte[frag_Size];
			try {
				inOut.seek(i*frag_Size);
				int j = inOut.read(data);
				data = subArray(data, 0,j);//remove extra space.
			} catch (IOException e) {
				//major problem
				e.printStackTrace();
			}
			f = FileFragment.buildFragment(data, FileFragment.DATA_FRG, numFrag, totFrags);

		}	
		return f;
	}

	public boolean saveFrag(FileFragment f){
		if(serv || done){
			//we're sending, or we're done.
			return true;
		}
		System.out.println("Save FRG");
		boolean ret = true;

		if(!f.getHash().isEmpty()){
			//if it has a hash
			String h = getHash(f.getData());
			if(!f.getHash().equals(h)){
				//ERROR! This is a major problem.
				System.err.println("ERROR diffrent hash values: "+f.getHash()+" & "+h);
				return false;
			}
		}
		
		if(have == null){
			have = new BitSet( f.getTotalPieces()+1 );
			have.set(f.getTotalPieces());
			totFrags = f.getTotalPieces();
			if(NetworkVars.debug){
				System.out.println("File Transfer Total Fragments set to:"+totFrags);
			}
		}
		

		int tmp = 0;
		switch(f.getType()){
		case FileFragment.DATA_FRG:
			try {
				if(f.getDataLen() > frag_Size){
					//size diffrence between fragment size and data,
				}else if(f.getDataLen() < frag_Size){
					//again size diffrence, this is less of a problem.
				}
				inOut.seek( f.getPiece()*frag_Size );
				inOut.write( f.getData() );
				have.set(f.getPiece(), true);
				
			} catch (IOException e) {
				//couldn't save. Problem.
				e.printStackTrace();
				ret = false;
			}
			break;
		case FileFragment.NAME_FRG:
			info[0]=f;
			System.out.println("File Transfer Name Fragment Captured");
			break;
		case FileFragment.HASH_FRG:
			hash = Util.getString(f.getData());
			break;
		case FileFragment.MISC_FRG:
			String data = Util.getString(f.getData());
			tmp = data.indexOf("d")+1;
			frag_Size = Integer.parseInt(data.substring(tmp,nextChar(data,tmp)));
			if(NetworkVars.debug){
				System.out.println("File Transfer Fragment Size set to:"+frag_Size);
			}
			tmp = data.indexOf("l")+1;
			try {
				long len = Long.parseLong(data.substring(tmp,nextChar(data,tmp)));
				inOut.setLength( len );
			} catch (NumberFormatException e1) {
				//no promblem, just abandon this
				e1.printStackTrace();
			} catch (IOException e1) {
				
				e1.printStackTrace();
			}

			break;
		}
		have.flip(0, totFrags+1);//make sure we flip it back.
		if(have.isEmpty()){//if we have all fragments, then this should be true
			try {
				String h = getHash(file);
				System.out.println("File Transfer Compare Hash: "+hash);
				if(!hash.equals(h)){
					System.err.println("End Hash Diffrent: "+h+" & "+hash);
				}
				inOut.close();
			} catch (IOException e) {
				//this should be thrown if it's already closed, and for no other reason (?)
				e.printStackTrace();
			}
			file.renameTo(new File(Util.getString(info[0].getData())));
			done = true;
		}
		have.flip(0,totFrags+1);
		
		return ret;
	}


	
	public String state(){
		String ret = "";
		for(int i=0; i<have.size(); i++){
			ret += (have.get(i)?"1":"0");
		}
		return ret;
	}
	
	public static int roundUp(double x){
		if( ((int)x) == x ){
			//it's a nice number
			return (int)x;
		}else{
			return 1+(int)x;
		}
	}

	public static byte[] subArray(byte[] b, int start, int end){
		byte[] ret = new byte[end-start];
		for(int i=0; i<ret.length; i++){
			ret[i] = b[i+start];
		}
		return ret;
	}

	public static int nextChar(String str, int i){
		int ret =str.length();
		
		for(int j=i; j<str.length(); j++){
			if( str.charAt(j) < '0' || str.charAt(j) > '9'){
				ret = j;
				break;
			}
		}
		
		return ret;
	}
	
	public static int nextNum(String str, int i){
		int ret =str.length();
		
		for(int j=i; j<str.length(); j++){
			if( str.charAt(j) > '0' && str.charAt(j) < '9'){
				ret = j;
				break;
			}
		}
		
		return ret;
	}
	

	/**
	 * Returns the hash of the file passed.
	 * @param f the file to get the hash of
	 * @return returns the hash, 32 characters, base 16, 
	 * @throws FileNotFoundException if the file does not exist
	 * @throws IOException if there is an io error
	 */
	public static String getHash(File f) throws FileNotFoundException, IOException{
		String ret = "";
		long sz = f.length();
		FileInputStream fIn = new FileInputStream(f);
		try {
			MessageDigest m=MessageDigest.getInstance(NetworkVars.encode);
			while (sz > 0){
				int i= 0;
				if(sz < NetworkVars.fileChunk){
					i = NetworkVars.fileChunk;
				}else{
					i= (int)sz;//i > networkvars.filechunk
				}
				byte[] data = new byte[i];
				i=0;
				i = fIn.read(data);
				sz -= i;
				if(i == 0 ){
					break;
				}
				m.update(data,0,i);

			}
			ret = new BigInteger(1,m.digest()).toString(16);
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		while(ret.length() < 32){
			ret = "0"+ret;
		}
		return ret;
	}

	public static String getHash(String data){
		return getHash(data.getBytes());
	}
	public static String getHash(byte[] data){
		String ret = "";
		try {
			MessageDigest m=MessageDigest.getInstance(NetworkVars.encode);
			m.update(data,0,data.length);
			ret = new BigInteger(1,m.digest()).toString(16);
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		while(ret.length() < 32){
			ret = "0"+ret;
		}
		return ret;
	}
	public static String getHash(ByteBuffer data){
		String ret = "";
		try {
			MessageDigest m=MessageDigest.getInstance(NetworkVars.encode);
			m.update(data);
			ret = new BigInteger(1,m.digest()).toString(16);
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		while(ret.length() < 32){
			ret = "0"+ret;
		}
		return ret;
	}

}
